/*
 * WavIO.java
 * 
 * This class runs in its own thread. When the run() function is called,
 * the contents of the 'temp' folder will be deleted, and recording will
 * begin (writing to a PCM with .raw extension, lacking WAV headers).
 * 
 * Recording will continue until 'recording' is set to 'false' (which is
 * triggered from the main GUI by clicking the 'stop' button), at which
 * point the file will be converted to a WAV by prefixing the appropriate
 * headers.
 * 
 * This class borrows heavily from this article:
 * 
 * http://www.thisisnotalabel.com/Reading-and-Writing-Wav-Files-in-Java.php
 * 
 * 
 *
 *
	 
	(the following notation is copied from...
	  	http://www.thisisnotalabel.com/Reading-and-Writing-Wav-Files-in-Java.php
	)
	  
    WAV File Specification
    FROM http://ccrma.stanford.edu/courses/422/projects/WaveFormat/
    
   The canonical WAVE format starts with the RIFF header:
   0         4   ChunkID          Contains the letters "RIFF" in ASCII form
                                  (0x52494646 big-endian form).
   4         4   ChunkSize        36 + SubChunk2Size, or more precisely:
                                  4 + (8 + SubChunk1Size) + (8 + SubChunk2Size)
                                  This is the size of the rest of the chunk
                                  following this number.  This is the size of the
                                  entire file in bytes minus 8 bytes for the
                                  two fields not included in this count:
                                  ChunkID and ChunkSize.
   8         4   Format           Contains the letters "WAVE"
                                  (0x57415645 big-endian form).

   The "WAVE" format consists of two subchunks: "fmt " and "data":
   The "fmt " subchunk describes the sound data's format:
   12        4   Subchunk1ID      Contains the letters "fmt "
                                  (0x666d7420 big-endian form).
   16        4   Subchunk1Size    16 for PCM.  This is the size of the
                                  rest of the Subchunk which follows this number.
   20        2   AudioFormat      PCM = 1 (i.e. Linear quantization)
                                  Values other than 1 indicate some
                                  form of compression.
   22        2   NumChannels      Mono = 1, Stereo = 2, etc.
   24        4   SampleRate       8000, 44100, etc.
   28        4   ByteRate         == SampleRate * NumChannels * BitsPerSample/8
   32        2   BlockAlign       == NumChannels * BitsPerSample/8
                                  The number of bytes for one sample including
                                  all channels. I wonder what happens when
                                  this number isn't an integer?
   34        2   BitsPerSample    8 bits = 8, 16 bits = 16, etc.

   The "data" subchunk contains the size of the data and the actual sound:
   36        4   Subchunk2ID      Contains the letters "data"
                                  (0x64617461 big-endian form).
   40        4   Subchunk2Size    == NumSamples * NumChannels * BitsPerSample/8
                                  This is the number of bytes in the data.
                                  You can also think of this as the size
                                  of the read of the subchunk following this
                                  number.
   44        *   Data             The actual sound data.

	NOTE TO READERS:
	
	The thing that makes reading wav files tricky is that java has no unsigned types.  This means that the
	binary data can't just be read and cast appropriately.  Also, we have to use larger types
	than are normally necessary.
	
	In many languages including java, an integer is represented by 4 bytes.  The issue here is
	that in most languages, integers can be signed or unsigned, and in wav files the  integers
	are unsigned.  So, to make sure that we can store the proper values, we have to use longs
	to hold integers, and integers to hold shorts.
	
	Then, we have to convert back when we want to save our wav data.
	
	It's complicated, but ultimately, it just results in a few extra functions at the bottom of
	this file.  Once you understand the issue, there is no reason to pay any more attention
	to it.
	
	
	ALSO:
	
	This code won't read ALL wav files.  This does not use to full specification.  It just uses
	a trimmed down version that most wav files adhere to.

*/

package com.android.UrbanRemix;

import java.io.*;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.text.SimpleDateFormat;
import java.util.*;

import android.app.Activity;
import android.content.SharedPreferences;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;

public class WavIO implements Runnable
{	
    private int myChunkSize;
    private int mySubChunk1Size;
    private int myFormat;
    private int myChannels;
    private int mySampleRate;
    private int myByteRate;
    private int myBlockAlign;
    private int myBitsPerSample;
    private int myDataSize;
    private int myRawFileData;
    
    private File rawFile, wavFile;
    private File folder, tempFolder, queueFolder;
    private String rawFileName, wavFileName;
    private PostSnapShot urbanRemix; 
    private int frequency;
    private int channelConfiguration;
    private static final int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
    private boolean isRecording;
    private boolean isPaused;
    private String folderPath, tempFolderPath, queueFolderPath, baseFileName;
    private String baseFileNameAndCurrentDate;
    
    // I made this public so that you can toss whatever you want in here
    // maybe a recorded buffer, maybe just whatever you want
    public byte[] myData;
    public static final String MYPREFS = "mySharedPreferences";
    public WavIO(PostSnapShot urbanRemix, String baseFileName)
    {	
    	super();
    	
    	this.urbanRemix = urbanRemix;
    	this.folderPath = urbanRemix.getFolderPath();
    	this.baseFileName = baseFileName;
    	this.isRecording = isPaused = false;
    	
    	this.folderPath = urbanRemix.getFolderPath();
    	this.tempFolderPath = urbanRemix.getTempFolderPath();
    	this.queueFolderPath = urbanRemix.getQueueFolderPath();
    	
    	queueFolder = new File(queueFolderPath);
    	folder = new File(folderPath);
    	tempFolder = new File(tempFolderPath);

    	// ------------------------------
    	// Set WAV file header parameters.
    	
    	mySubChunk1Size = 16;
    	myFormat = 1;
    	myChannels = 1;
    	mySampleRate = 44100;
    	myByteRate = 88200;
    	myBlockAlign = 2;
    	myBitsPerSample = 16; 	
    }

        /*
     * This fn sets the file name using the base file name provided in the constructor,
     * prefixing the current date and time to the beginning of the file name.
     */
    
    private void setNewFileName()
    {
    	Calendar cal = Calendar.getInstance();
    	SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
    	String currentDateAndTime = sdf.format(cal.getTime());
    	baseFileNameAndCurrentDate = baseFileName + "-"+ currentDateAndTime;
    	
    	this.wavFileName = tempFolderPath+"/"+baseFileNameAndCurrentDate+".wav";
    	this.rawFileName = tempFolderPath+"/"+baseFileNameAndCurrentDate+".raw";
    	rawFile = new File(rawFileName);
    	wavFile = new File(wavFileName);
    }
    
    public File getQueueFolder() { return this.queueFolder; }
    public File getTempFolder() { return this.tempFolder; }
    
    public void lockFile(File f)
    {
    	try {
            // Get a file channel for the file
            FileChannel channel = new RandomAccessFile(f, "rw").getChannel();
        
            // Use the file channel to create a lock on the file.
            // This method blocks until it can retrieve the lock.
            FileLock lock = channel.lock();
        
            // Try acquiring the lock without blocking. This method returns
            // null or throws an exception if the file is already locked.
            try
            {
                lock = channel.tryLock();
            } catch (OverlappingFileLockException e)
            {
                // File is already locked in this thread or virtual machine
            }
        
            // Release the lock
            lock.release();
        
            // Close the file
            channel.close();
        } catch (Exception e) {
        }
    }
    
    public void run()
    {
    	clearAudioFiles();
    	setNewFileName();
    	recordAndSaveWave();
    }
    
    // -------
    // Delete all existing wav or raw files in the temp folder.
    
    private void clearAudioFiles()
    {
    	File[] tempfiles;
    	tempfiles = tempFolder.listFiles();
    	
    	for (int i = 0; i < tempfiles.length; i++)
    	{
    		String temppath = tempfiles[i].toString();
    		String ext = temppath.substring(temppath.lastIndexOf(".")+1, temppath.length());
    		
    		if ((ext.equals(new String("wav"))) || (ext.equals(new String("raw")))) tempfiles[i].delete();
    		
    	}
    }
    
    // ----------
    // Record the raw file. When 'recording' is finally set to 'false' (by clicking the 'stop'
    // button in the main GUI), convert it to a WAV by appending the WAV headers,
    // pause the HttpFileUploader -- so it doesn't attempt to upload any
    // half-copied files -- then copy it to the 'queue' folder. After the file has been copied,
    // uploading can resume (set 'paused' to false). 
    
    private void recordAndSaveWave()
    {
    	recordRawFile();
    //	urbanRemix.qUploader.setPaused(true);
    	saveAsWav();
    //	urbanRemix.qUploader.setPaused(false);
    }
    
    private void recordRawFile()
    {    	        
    	if (this.rawFile == null) { throw new IllegalStateException("rawFile is null"); }
        BufferedOutputStream bufferedStreamInstance = null;
        
        if (rawFile.exists()) { rawFile.delete(); }
        try { rawFile.createNewFile(); }
        catch (IOException e)
        {
             throw new IllegalStateException("Cannot create file: " + rawFile.toString());
        }
        
        try
        {
             bufferedStreamInstance = new BufferedOutputStream(
                       new FileOutputStream(this.rawFile));
        }
        catch (FileNotFoundException e)
        {
             throw new IllegalStateException("Cannot Open File", e);
        }
        
        DataOutputStream dataOutputStreamInstance =
             new DataOutputStream(bufferedStreamInstance);
        
        // We're important...
        android.os.Process
                  .setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);

        // Allocate Recorder and Start Recording...
        int bufferRead = 0;
        int bufferSize = AudioRecord.getMinBufferSize(this.getFrequency(),
                  this.getChannelConfiguration(), this.getAudioEncoding());
        
        	AudioRecord recordInstance = new AudioRecord(
        
                  MediaRecorder.AudioSource.MIC, frequency,
                            channelConfiguration, audioEncoding,
                  bufferSize);
        
        byte[] tempBuffer = new byte[bufferSize];
        
        recordInstance.startRecording();
        
        while (this.isRecording) {
             
             bufferRead = recordInstance.read(tempBuffer, 0, bufferSize);
		             if (bufferRead == AudioRecord.ERROR_INVALID_OPERATION) {
		                  throw new IllegalStateException(
		                            "read() returned AudioRecord.ERROR_INVALID_OPERATION");
		             } else if (bufferRead == AudioRecord.ERROR_BAD_VALUE) {
		                  throw new IllegalStateException(
		                            "read() returned AudioRecord.ERROR_BAD_VALUE");
		             } else if (bufferRead == AudioRecord.ERROR_INVALID_OPERATION) {
		                  throw new IllegalStateException(
		                            "read() returned AudioRecord.ERROR_INVALID_OPERATION");
		             }
             try {
                  for (int idxBuffer = 0; idxBuffer < bufferRead; ++idxBuffer) {
                       dataOutputStreamInstance.write(tempBuffer[idxBuffer]);
                  }
             } catch (IOException e) {
                 // throw new IllegalStateException(
                 //      "dataOutputStreamInstance.writeShort(curVal)");
             }
             
        }

        // Close resources...
        recordInstance.stop();

        try {
             bufferedStreamInstance.close();
        } catch (IOException e) {
             throw new IllegalStateException("Cannot close buffered writer.");
        }
    }
    // END recordSound()    
    
    // -------
    // Convert the .raw PCM file into .wav format by appending headers
    
    public boolean saveAsWav()
    {
          try
            {        	  
        	  		myChunkSize = (int)rawFile.length() + 36;
        	  		myRawFileData = (int)rawFile.length();
        	  
                    DataOutputStream outFile  = new DataOutputStream(new FileOutputStream(wavFileName));

                    // write the WAV file per the WAV file format
                    outFile.writeBytes("RIFF");                                        	// 00 - RIFF
                    outFile.write(intToByteArray(myChunkSize), 0, 4);	// 04 - how big is the rest of this file?
                    outFile.writeBytes("WAVE");                                        	// 08 - WAVE
                    outFile.writeBytes("fmt ");                                        	// 12 - fmt
                    outFile.write(intToByteArray(mySubChunk1Size), 0, 4);				// 16 - size of this chunk
                    outFile.write(intToByteArray(myFormat), 0, 2);						// 20 - what is the audio format? 1 for PCM = Pulse Code Modulation
                    outFile.write(intToByteArray(myChannels), 0, 2);					// 22 - mono or stereo? 1 or 2?  (or 5 or ???)
                    outFile.write(intToByteArray(mySampleRate), 0, 4);					// 24 - samples per second (numbers per second)
                    outFile.write(intToByteArray(myByteRate), 0, 4);					// 28 - bytes per second
                    outFile.write(intToByteArray(myBlockAlign), 0, 2);					// 32 - # of bytes in one sample, for all channels
                    outFile.write(intToByteArray(myBitsPerSample), 0, 2);				// 34 - how many bits in a sample(number)?  usually 16 or 24
                    outFile.writeBytes("data");                                        	// 36 - data
                    outFile.write(intToByteArray(myRawFileData), 0, 4);                	// 40 - how big is this data chunk
                                    
                    DataInputStream inFile = new DataInputStream(new FileInputStream(rawFileName));
                    myData = new byte[(int)rawFile.length()];
                    inFile.read(myData);                                        
                    outFile.write(myData);												// 44 - the actual data itself - just a long string of numbers                    
                    inFile.close();
                    rawFile.delete();
                    outFile.close();
            }
            catch(Exception e)
            {
                    System.out.println(e.getMessage());
                    return false;
            }
            return true;
    }    

    // return a printable summary of the wav file
    public String getSummary()
    {
            //String newline = System.getProperty("line.separator");
            String newline = "<br>";
            String summary = "<html>Format: " + myFormat + newline + "Channels: " + myChannels + newline + "SampleRate: " + mySampleRate + newline + "ByteRate: " + myByteRate + newline + "BlockAlign: " + myBlockAlign + newline + "BitsPerSample: " + myBitsPerSample + newline + "DataSize: " + myDataSize + "</html>";
            return summary;
    }

	//===========================
	//CONVERT BYTES TO JAVA TYPES
	//===========================

    // these two routines convert a byte array to a unsigned short
    public static int byteArrayToInt(byte[] b)
    {
            int start = 0;
            int low = b[start] & 0xff;
            int high = b[start+1] & 0xff;
            return (int)( high << 8 | low );
    }


    // these two routines convert a byte array to an unsigned integer
    public static long byteArrayToLong(byte[] b)
    {
            int start = 0;
            int i = 0;
            int len = 4;
            int cnt = 0;
            byte[] tmp = new byte[len];
            for (i = start; i < (start + len); i++)
            {
                    tmp[cnt] = b[i];
                    cnt++;
            }
            long accum = 0;
            i = 0;
            for ( int shiftBy = 0; shiftBy < 32; shiftBy += 8 )
            {
                    accum |= ( (long)( tmp[i] & 0xff ) ) << shiftBy;
                    i++;
            }
            return accum;
    }


	//===========================
	//CONVERT JAVA TYPES TO BYTES
	//===========================
    // returns a byte array of length 4
    private static byte[] intToByteArray(int i)
    {
            byte[] b = new byte[4];
            b[0] = (byte) (i & 0x00FF);
            b[1] = (byte) ((i >> 8) & 0x000000FF);
            b[2] = (byte) ((i >> 16) & 0x000000FF);
            b[3] = (byte) ((i >> 24) & 0x000000FF);
            return b;
    }

    // convert a short to a byte array
    public static byte[] shortToByteArray(short data)
    {
            return new byte[]{(byte)(data & 0xff),(byte)((data >>> 8) & 0xff)};
    }


	//============================
	//ACCESSORS AND MUTATORS -----
	//============================
	
	// @param isRecording 
	public void setRecording(boolean isRecording) { this.isRecording = isRecording; }
	
	// @return isRecording
	public boolean isRecording(){ return isRecording; }
	
	// @param frequency
	public void setFrequency(int frequency) { this.frequency = frequency; }
	
	// @return frequency
	public int getFrequency() { return frequency; }
	
	// @param channelConfiguration
	public void setChannelConfiguration(int channelConfiguration) { this.channelConfiguration = channelConfiguration; }
	
	// @return channelConfiguration
	public int getChannelConfiguration() { return channelConfiguration; }
	
	// @return audioEncoding
	public int getAudioEncoding() { return audioEncoding; }
	
	// @param isPaused (volatile)
	public void setPaused(boolean isPaused) {  this.isPaused = isPaused; }
	
	// @return isPaused (volatile)
	public boolean isPaused() { return isPaused; }
	
	// @return wavFileName
	public String getWavFileName() { return wavFileName; }
	
	// @return baseFileNameAndCurrentData 
	public String getBaseFileNameAndCurrentDate() { 	
	return this.baseFileNameAndCurrentDate; }

}